package auth

import (
	"context"
	"runtime"
	"strconv"
	"sync"

	"github.com/casbin/casbin/v2/persist"
	client "go.etcd.io/etcd/client/v3"
)

type Watcher struct {
	// lock for callback
	lock        sync.RWMutex
	client      *client.Client
	running     bool
	callback    func(string)
	keyName     string
	lastSentRev int64
}

// finalizer is the destructor for Watcher.
func finalizer(w *Watcher) {
	w.running = false
}

// NewWatcher is the constructor for Watcher.
// endpoints is the endpoints for etcd clusters.
func NewWatcher(cli *client.Client, profix string, daemon string) (persist.Watcher, error) {
	keyName := profix + MiddChan + daemon
	w := &Watcher{}
	w.running = true
	w.callback = nil
	w.keyName = keyName
	w.client = cli

	// Call the destructor when the object is released.
	runtime.SetFinalizer(w, finalizer)

	go func() {
		_ = w.startWatch()
	}()

	return w, nil
}

// Close closes the Watcher.
func (w *Watcher) Close() {
	finalizer(w)
}

// SetUpdateCallback sets the callback function that the watcher will call
// when the policy in DB has been changed by other instances.
// A classic callback is Enforcer.LoadPolicy().
func (w *Watcher) SetUpdateCallback(callback func(string)) error {
	w.lock.Lock()
	defer w.lock.Unlock()
	w.callback = callback
	return nil
}

// Update calls the update callback of other instances to synchronize their policy.
// It is usually called after changing the policy in DB, like Enforcer.SavePolicy(),
// Enforcer.AddPolicy(), Enforcer.RemovePolicy(), etc.
func (w *Watcher) Update() error {
	w.lock.Lock()
	defer w.lock.Unlock()
	resp, err := w.client.Put(context.TODO(), w.keyName, "")
	if err == nil {
		w.lastSentRev = resp.Header.GetRevision()
	}
	return err
}

// startWatch is a goroutine that watches the policy change.
func (w *Watcher) startWatch() error {
	watcher := w.client.Watch(context.Background(), w.keyName)
	for res := range watcher {
		t := res.Events[0]
		if t.IsCreate() || t.IsModify() {
			w.lock.RLock()
			//ignore self update
			if rev := t.Kv.ModRevision; rev > w.lastSentRev && w.callback != nil {
				w.callback(strconv.FormatInt(rev, 10))
			}
			w.lock.RUnlock()
		}
	}
	return nil
}
